<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="ncacK" id="ncacK"><span data-lake-id="uc69e01ff" id="uc69e01ff">典型回答</span></h1>
  <p data-lake-id="u74e7dc31" id="u74e7dc31"><br></p>
  <p data-lake-id="u83773c1f" id="u83773c1f"><span data-lake-id="uf4f322e0" id="uf4f322e0">我们知道，想要把高级语言转变成计算机认识的机器语言有两种方式，分别是编译和解释，虽然Java转成机器语言的过程中有一个步骤是要编译成字节码，但是，这里的字节码并不能在机器上直接执行。</span></p>
  <p data-lake-id="ue9fc129b" id="ue9fc129b"><span data-lake-id="u1af74f79" id="u1af74f79">​</span><br></p>
  <p data-lake-id="uad62ecce" id="uad62ecce"><span data-lake-id="u976c46e2" id="u976c46e2">所以，JVM中内置了解释器(interpreter)，在运行时对字节码进行解释翻译成机器码，然后再执行。</span></p>
  <p data-lake-id="u0896c8f1" id="u0896c8f1"><span data-lake-id="u7ee8407a" id="u7ee8407a">​</span><br></p>
  <p data-lake-id="u16ceb25c" id="u16ceb25c"><span data-lake-id="u8f8715e0" id="u8f8715e0">解释器的执行方式是一边翻译，一边执行，因此执行效率很低。为了解决这样的低效问题，HotSpot引入了JIT技术（Just-In-Time）。</span></p>
  <p data-lake-id="u6cc82f34" id="u6cc82f34"><span data-lake-id="uac87b310" id="uac87b310">​</span><br></p>
  <p data-lake-id="u80479ddb" id="u80479ddb"><span data-lake-id="ud72c64cd" id="ud72c64cd">有了JIT技术之后，JVM还是通过解释器进行解释执行。但是，</span><strong><span data-lake-id="u92fe1307" id="u92fe1307">当JVM发现某个方法或代码块运行时执行的特别频繁的时候，就会认为这是“热点代码”（Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码，并进行优化，然后再把翻译后的机器码缓存起来，以备下次使用。</span></strong></p>
  <p data-lake-id="u91ec889a" id="u91ec889a"><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/5378072/1667466529876-3467ee10-0d71-4f1a-a42c-76fdcfe224ad.jpeg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_40%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ub2a0e2d0" id="ub2a0e2d0"><span data-lake-id="uf8f5ac5a" id="uf8f5ac5a"><br><br></span></p>
  <h1 data-lake-id="pZ59V" id="pZ59V"><span data-lake-id="u8bb4afaf" id="u8bb4afaf">扩展知识</span></h1>
  <p data-lake-id="uecc58271" id="uecc58271"><br></p>
  <p data-lake-id="u8be0bdf0" id="u8be0bdf0"><span data-lake-id="ua79990c3" id="ua79990c3">HotSpot虚拟机中内置了两个JIT编译器：Client Complier和Server Complier，分别用在客户端和服务端，目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。</span></p>
  <p data-lake-id="ud557e86a" id="ud557e86a"><span data-lake-id="u7b1a298d" id="u7b1a298d">​</span><br></p>
  <p data-lake-id="u2b75b5ea" id="u2b75b5ea"><span data-lake-id="u80f9afff" id="u80f9afff">当 JVM 执行代码时，它并不立即开始编译代码（因为Java默认是解释执行的）。首先，如果这段代码本身在将来只会被执行一次，那么从本质上看，编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说，要快很多。第二个原因是最优化，当 JVM 执行某一方法或遍历循环的次数越多，就会更加了解代码结构，那么 JVM 在编译代码的时候就做出相应的优化。</span></p>
  <p data-lake-id="u6734fc1a" id="u6734fc1a"><span data-lake-id="u22e90b05" id="u22e90b05">​</span><br></p>
  <p data-lake-id="ueb34ce43" id="ueb34ce43"><span data-lake-id="ude02099e" id="ude02099e">​</span><br></p>
  <p data-lake-id="u699f13be" id="u699f13be"><span data-lake-id="u278e8df2" id="u278e8df2">在机器上，执行</span><span data-lake-id="u616903cd" id="u616903cd">java -version</span><span data-lake-id="uddea6e0f" id="uddea6e0f">命令就可以看到自己安装的JDK中JIT是哪种模式:</span></p>
  <p data-lake-id="u87b817f0" id="u87b817f0"><span data-lake-id="u5d2a257f" id="u5d2a257f">​</span><br></p>
  <p data-lake-id="u558bed10" id="u558bed10"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1667466581344-a66e636c-af2a-4a78-9b0f-be259b5fca0f.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_13%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ud32724e8" id="ud32724e8"><br></p>
  <p data-lake-id="u811ff330" id="u811ff330"><span data-lake-id="u810ba8d6" id="u810ba8d6">上图是我的机器上安装的jdk1.8，可以看到，他是Server Compile，但是，需要说明的是，无论是Client Complier还是Server Complier，解释器与编译器的搭配使用方式都是混合模式，即上图中的mixed mode。</span></p>
  <h2 data-lake-id="CTuOd" id="CTuOd"><span data-lake-id="u45465445" id="u45465445">热点检测</span></h2>
  <p data-lake-id="u933c78f9" id="u933c78f9"><br></p>
  <p data-lake-id="ueedcaf77" id="ueedcaf77"><span data-lake-id="u34ae053a" id="u34ae053a">上面我们说过，要想触发JIT，首先需要识</span><strong><span data-lake-id="u4fb28425" id="u4fb28425">别出热点代码</span></strong><span data-lake-id="u25362970" id="u25362970">。目前主要的热点代码识别方式是热点探测（Hot Spot Detection），有以下两种：</span></p>
  <p data-lake-id="u2807b6e4" id="u2807b6e4"><span data-lake-id="ubbbac777" id="ubbbac777">​</span><br></p>
  <p data-lake-id="uc17cd07d" id="uc17cd07d"><strong><span data-lake-id="u5c9e1d4e" id="u5c9e1d4e">1、基于采样的方式探测（Sample Based Hot Spot Detection) </span></strong><span data-lake-id="u90931af7" id="u90931af7">：周期性检测各个线程的栈顶，发现某个方法经常出现在栈顶，就认为是热点方法。好处就是简单，缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。</span></p>
  <p data-lake-id="u8f849552" id="u8f849552"><span data-lake-id="ue9d18102" id="ue9d18102">​</span><br></p>
  <p data-lake-id="u98c7fdff" id="u98c7fdff"><strong><span data-lake-id="ubcdf7b42" id="ubcdf7b42">2、基于计数器的热点探测（Counter Based Hot Spot Detection)</span></strong><span data-lake-id="ubdf5fa3c" id="ubdf5fa3c">。采用这种方法的虚拟机会为每个方法，甚至是代码块建立计数器，统计方法的执行次数，某个方法超过阀值就认为是热点方法，触发JIT编译。</span></p>
  <p data-lake-id="ud43dc124" id="ud43dc124"><span data-lake-id="u97c515ce" id="u97c515ce">​</span><br></p>
  <p data-lake-id="ua4d22aa4" id="ua4d22aa4"><span data-lake-id="u3e8f13ea" id="u3e8f13ea">在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法，因此它为每个方法准备了两个计数器：方法调用计数器和回边计数器。</span></p>
  <p data-lake-id="u6577211e" id="u6577211e"><span data-lake-id="u2ab8cc84" id="u2ab8cc84">​</span><br></p>
  <p data-lake-id="u3adcf11e" id="u3adcf11e"><span data-lake-id="u0b046bbb" id="u0b046bbb">方法计数器。顾名思义，就是记录一个方法被调用次数的计数器。</span></p>
  <p data-lake-id="u81c1dae8" id="u81c1dae8"><span data-lake-id="ufdc66e2e" id="ufdc66e2e">回边计数器。是记录方法中的for或者while的运行次数的计数器。</span></p>
  <p data-lake-id="uf288047d" id="uf288047d"><span data-lake-id="ub2089f16" id="ub2089f16">​</span><br></p>
  <h2 data-lake-id="SGiKS" id="SGiKS"><span data-lake-id="u5b167853" id="u5b167853">编译优化</span></h2>
  <p data-lake-id="u9f77c083" id="u9f77c083"><span data-lake-id="ud9be35a7" id="ud9be35a7">前面提到过，JIT除了具有缓存的功能外，还会对代码做各种优化。说到这里，不得不佩服HotSpot的开发者，他们在JIT中对于代码优化真的算是面面俱到了。</span></p>
  <p data-lake-id="u91d99591" id="u91d99591"><span data-lake-id="u007bf95c" id="u007bf95c">​</span><br></p>
  <p data-lake-id="uda77a472" id="uda77a472"><span data-lake-id="u5dd767ce" id="u5dd767ce">主要的优化有：逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除。接下来挑几个重点的介绍一下。</span></p>
  <p data-lake-id="u57d1483c" id="u57d1483c"><br></p>
  <h3 data-lake-id="S0gWS" id="S0gWS"><span data-lake-id="u00f74580" id="u00f74580">逃逸分析</span></h3>
  <p data-lake-id="u9a311b6b" id="u9a311b6b"><br></p>
  <p data-lake-id="ua24554f3" id="ua24554f3"><br></p>
  <h3 data-lake-id="k0Z4l" id="k0Z4l"><span data-lake-id="u23e50d56" id="u23e50d56">锁消除</span></h3>
  <p data-lake-id="u7d759a7a" id="u7d759a7a"><span data-lake-id="u94a1d44a" id="u94a1d44a">在动态编译同步块的时候，JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。</span></p>
  <p data-lake-id="uc620247e" id="uc620247e"><span data-lake-id="u304028c1" id="u304028c1">​</span><br></p>
  <p data-lake-id="u3ea63f37" id="u3ea63f37"><span data-lake-id="u48343eac" id="u48343eac">如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问，那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略，也叫</span><strong><span data-lake-id="ub411a8c7" id="ub411a8c7">锁消除</span></strong><span data-lake-id="u87b72224" id="u87b72224">。</span></p>
  <p data-lake-id="u03087555" id="u03087555"><span data-lake-id="u9393b93c" id="u9393b93c">​</span><br></p>
  <p data-lake-id="u016142b3" id="u016142b3"><span data-lake-id="u11eb23aa" id="u11eb23aa">如以下代码：</span></p>
  <pre lang="java"><code>
public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}

</code></pre>
  <p data-lake-id="u7f3060f5" id="u7f3060f5"><span data-lake-id="ub26b7a63" id="ub26b7a63">代码中对hollis这个对象进行加锁，但是hollis对象的生命周期只在f()方法中，并不会被其他线程所访问到，所以在JIT编译阶段就会被优化掉。优化成：</span></p>
  <p data-lake-id="u60272470" id="u60272470"><span data-lake-id="ud5987add" id="ud5987add">​</span><br></p>
  <pre lang="java"><code>
public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}
</code></pre>
  <p data-lake-id="ub3e3e93c" id="ub3e3e93c"><span data-lake-id="u52dd3056" id="u52dd3056">​</span><br></p>
  <p data-lake-id="uaafc55bc" id="uaafc55bc"><span data-lake-id="u011d57c3" id="u011d57c3">所以，在使用synchronized的时候，如果JIT经过逃逸分析之后发现并无线程安全问题的话，就会做锁消除。</span></p>
  <p data-lake-id="u19051f9f" id="u19051f9f"><span data-lake-id="u94cd4660" id="u94cd4660">​</span><br></p>
  <h3 data-lake-id="MsOh2" id="MsOh2"><span data-lake-id="u05b4ea51" id="u05b4ea51">标量替换&amp;栈上分配</span></h3>
  <p data-lake-id="ueb7c8e95" id="ueb7c8e95"><br></p>
  <p data-lake-id="ua83b77c9" id="ua83b77c9"><br></p>
  <p data-lake-id="u952bdbaf" id="u952bdbaf"><br></p>
  <h3 data-lake-id="ovn99" id="ovn99"><span data-lake-id="u73c07e5f" id="u73c07e5f">方法内联</span></h3>
  <p data-lake-id="u50181ee4" id="u50181ee4"><br></p>
  <p data-lake-id="uea22e006" id="uea22e006"><span data-lake-id="u46eca253" id="u46eca253">方法内联是Java中的一个优化技术，即时编译器（JIT）用它来提高程序的运行效率。在Java中，方法内联意味着将一个方法的代码直接插入到调用它的地方，从而避免了方法调用的开销。这种优化对于小型且频繁调用的方法特别有用。</span></p>
  <p data-lake-id="uc0ed53cc" id="uc0ed53cc"><span data-lake-id="u5d5274d6" id="u5d5274d6">​</span><br></p>
  <pre lang="java"><code>
public class InlineExample {
    public int add(int a, int b) {
        return a + b;
    }

    public void exampleMethod() {
        int result = add(5, 3);
        // 其他操作
    }
}
</code></pre>
  <p data-lake-id="u23ee6e4f" id="u23ee6e4f"><br></p>
  <p data-lake-id="u98a16761" id="u98a16761"><span data-lake-id="u1049d812" id="u1049d812">在这个例子中，add 方法很简单，JIT 编译器可能会选择将 add方法 直接内联在 exampleMethod 中，避免了对 add 方法的实际调用。这样可以减少调用开销，提高程序的执行速度，类似</span></p>
  <p data-lake-id="uf5b5e62d" id="uf5b5e62d"><span data-lake-id="u9403f0d4" id="u9403f0d4">​</span><br></p>
  <pre lang="java"><code>
public class InlineExample {

    public void exampleMethod() {
        int result = 5 + 3; // 这里直接使用了add方法的内部逻辑
        // 其他操作
    }
}

</code></pre>
  <p data-lake-id="uaa72e858" id="uaa72e858"><br></p>
  <h2 data-lake-id="jjZ8n" id="jjZ8n"><span data-lake-id="uc78f0454" id="uc78f0454">JIT优化可能带来的问题</span></h2>
  <p data-lake-id="ueb623b70" id="ueb623b70"><span data-lake-id="uef952398" id="uef952398">大家理解了JIT编译的原理之后，其实可以知道，</span><strong><span data-lake-id="u1cb6fe22" id="u1cb6fe22">JIT优化是在运行期进行的</span></strong><span data-lake-id="u3ead99b7" id="u3ead99b7">，并且也不是Java进程刚一启动就能优化的，是需要先执行一段时间的，因为他需要先知道哪些是热点代码。</span></p>
  <p data-lake-id="uc7ffb1e7" id="uc7ffb1e7"><span data-lake-id="u7f72a622" id="u7f72a622">​</span><br></p>
  <p data-lake-id="ud52fa8c3" id="ud52fa8c3"><span data-lake-id="u0e3c91a6" id="u0e3c91a6">所以，在JIT优化开始之前，我们的所有请求，都是要经过解释执行的，这个过程就会相对慢一些。</span></p>
  <p data-lake-id="ua6ff18c8" id="ua6ff18c8"><span data-lake-id="u0e107d8f" id="u0e107d8f">​</span><br></p>
  <p data-lake-id="ud5035789" id="ud5035789"><span data-lake-id="u5af2137d" id="u5af2137d">而且，如果你们的应用的请求量比较大的的话，这种问题就会更加明显，在应用启动过程中，会有大量的请求过来，这就会导致解释器持续的在努力工作。</span></p>
  <p data-lake-id="u9b4e28fa" id="u9b4e28fa"><span data-lake-id="u10ece894" id="u10ece894">​</span><br></p>
  <p data-lake-id="u11d0ed6f" id="u11d0ed6f"><span data-lake-id="u8774f821" id="u8774f821">一旦解释器对CPU资源占用比较大的话，就会间接的导致CPU、LOAD等飙高，导致应用的性能进一步下降。这也是为什么很多应用在发布过程中，会出现刚刚重启好的应用会发生大量的超时问题了。</span></p>
  <p data-lake-id="u6cdb2bd9" id="u6cdb2bd9"><span data-lake-id="u26593f2f" id="u26593f2f">​</span><br></p>
  <p data-lake-id="ud5939798" id="ud5939798"><span data-lake-id="ue37be17a" id="ue37be17a">而随着请求的不断增多，JIT优化就会被触发，这就是使得后续的热点请求的执行可能就不需要在通过解释执行了，直接运行JIT优化后缓存的机器码就行了。</span></p>
  <p data-lake-id="uc04301f0" id="uc04301f0"><span data-lake-id="ufe61e79d" id="ufe61e79d">​</span><br></p>
  <h3 data-lake-id="cHAoz" id="cHAoz"><span data-lake-id="ue17ed3fb" id="ue17ed3fb">如何解决</span></h3>
  <p data-lake-id="uf52d510f" id="uf52d510f"><span data-lake-id="u525b03ee" id="u525b03ee">那么，怎么解决这样的问题呢？</span></p>
  <p data-lake-id="u49657cfa" id="u49657cfa"><span data-lake-id="u7e7caf73" id="u7e7caf73">​</span><br></p>
  <p data-lake-id="u379ef5f5" id="u379ef5f5"><span data-lake-id="u58b6599b" id="u58b6599b">主要有两种思路：</span></p>
  <p data-lake-id="u8bcb0ba2" id="u8bcb0ba2"><span data-lake-id="u49a7642c" id="u49a7642c">​</span><br></p>
  <p data-lake-id="u34b373d1" id="u34b373d1"><strong><span data-lake-id="u4cc136bd" id="u4cc136bd">1、提升JIT优化的效率</span></strong></p>
  <p data-lake-id="ua2bd2e30" id="ua2bd2e30"><strong><span data-lake-id="u1b26c841" id="u1b26c841">2、降低瞬时请求量</span></strong></p>
  <p data-lake-id="u367689f3" id="u367689f3"><span data-lake-id="uc216e685" id="uc216e685">​</span><br></p>
  <p data-lake-id="u0d983030" id="u0d983030"><span data-lake-id="ue25cf213" id="ue25cf213">在提升JIT优化效率的设计上，大家可以了解一下阿里研发的JDK——Dragonwell。</span></p>
  <p data-lake-id="u4a9d1ccf" id="u4a9d1ccf"><span data-lake-id="u5c109602" id="u5c109602">​</span><br></p>
  <p data-lake-id="u57f298c1" id="u57f298c1"><span data-lake-id="ua00ee034" id="ua00ee034">这个相比OpenJDK提供了一些专有特性，其中一项叫做</span><span data-lake-id="u1d6535b3" id="u1d6535b3">JwarmUp</span><span data-lake-id="uac7d2cfd" id="uac7d2cfd">的技术就是解决JIT优化效率的问题的。</span></p>
  <p data-lake-id="u7d258cfd" id="u7d258cfd"><span data-lake-id="u9c5370e8" id="u9c5370e8">​</span><br></p>
  <p data-lake-id="u17bed081" id="u17bed081"><span data-lake-id="u5ff44ea3" id="u5ff44ea3">这个技术主要是通过记录Java应用上一次运行时候的编译信息到文件中，在下次应用启动时，读取该文件，从而在流量进来之前，提前完成类的加载、初始化和方法编译，从而跳过解释阶段，直接执行编译好的机器码。</span></p>
  <p data-lake-id="u8753cc27" id="u8753cc27"><span data-lake-id="u6af7c738" id="u6af7c738">​</span><br></p>
  <p data-lake-id="u09b83bd7" id="u09b83bd7"><span data-lake-id="uc15a58aa" id="uc15a58aa">除了针对JDK做优化之外，还可以采用另外一种方式来解决这个问题，那就是做预热。</span></p>
  <p data-lake-id="uc0301728" id="uc0301728"><span data-lake-id="u75fa85ff" id="u75fa85ff">​</span><br></p>
  <p data-lake-id="u25b85a20" id="u25b85a20"><span data-lake-id="udff70345" id="udff70345">很多人都听说过缓存预热，其实思想是类似的。</span></p>
  <p data-lake-id="u8f508165" id="u8f508165"><span data-lake-id="u33861a00" id="u33861a00">​</span><br></p>
  <p data-lake-id="u08c95a1e" id="u08c95a1e"><span data-lake-id="u78b7b6ad" id="u78b7b6ad">就是说在应用刚刚启动的时候，通过调节负载均衡，不要很快的把大流量分发给他，而是先分给他一小部分流量，通过这部分流量来触发JIT优化，等优化好了之后，再把流量调大。<br><br></span></p>
  <p data-lake-id="ub2ed4b30" id="ub2ed4b30"><span data-lake-id="u3d0b2861" id="u3d0b2861">​</span><br></p>
 </body>
</html>